package org.hackystat.devcathlon.event.impl;

import javax.xml.datatype.XMLGregorianCalendar;

import org.hackystat.dailyprojectdata.client.DailyProjectDataClient;
import org.hackystat.dailyprojectdata.resource.coverage.jaxb.CoverageDailyProjectData;
import org.hackystat.devcathlon.event.EventException;
import org.hackystat.devcathlon.event.EventScore;
import org.hackystat.devcathlon.event.Event;
import org.hackystat.devcathlon.match.Match;
import org.hackystat.devcathlon.team.Team;
import org.hackystat.sensorbase.client.SensorBaseClient;
import org.hackystat.sensorbase.resource.projects.jaxb.Project;
import org.hackystat.sensorbase.resource.sensordata.jaxb.Property;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataIndex;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataRef;
import org.hackystat.utilities.tstamp.Tstamp;

/**
 * Awards points if the team has increased coverage after committing code, deducts 
 * points if coverage has decreased after committing code. 
 * <p>
 * Requires the following properties in the configuration.xml file for this event:
 * <ul>
 * <li> WakeupInterval: How frequently this event is invoked, and how far back it looks for data.
 * <li> BaseScoreWeight: An integer multiplied by the base score to get the base points.  
 * <li> UpperThreshold: An integer indicating the threshold for "high" coverage, such as 90.
 * <li> TypeOfCoverage: A string indicating the kind of coverage to retrieve, such as "Line". 
 * <li> FirstTimeOverThresholdWeight: An integer multiplied by the FirstTimeOverThreshold bonus
 * score to determine the bonus points. 
 * </ul>
 * <p> 
 * Specification: 
 * <ul>  
 * <li> Checks all Build sensor data since the last wakeup interval for the first build sensor data 
 * instance with a Type property equal to "continuous.integration".  Records the timestamp 
 * associated with that sensor data.
 * <li> Retrieves the Coverage DPD associated with this project.  Checks the timestamp 
 * associated with it to see if this data has been generated after the CI build event. 
 * <li> If Coverage has been generated after the build, then award base and bonus points, otherwise 
 * do nothing. 
 * <li> Base Score: 0 if over UpperThreshold or if no previous coverage data exists. 
 * Otherwise base score is between -10 and +10 depending upon percentage increase or decrease since
 * last coverage measurement.  
 * <li> Bonus Score:  Award Bonus Score of type "FirstTimeOverThreshold" if this is the first
 * recorded time that this team is over UpperThreshold. 
 * </ul>
 * @author Philip Johnson
 *
 */
public class KeepTheCoverageHigh extends Event {
  
  /** The last stored value for coverage. */
  private int lastCoverage = 0;
  /** Whether or not we have a prior coverage value for this team and match. */
  boolean hasLastCoverage = false;
  /** Whether or not this team has received the FirstTimeOverThreshold bonus point. */
  boolean hasFirstTimeBonus = false;
  /** The unique name for this event. */
  public static final String eventName = "Keep The Coverage High";
  /** The property key for the upper threshold. */
  public static final String upperThresholdKey = "UpperThreshold";
  /** The actual upper threshold. */
  private int upperThreshold;
  /** The property key for the type of coverage. */
  public static final String typeOfCoverageKey = "TypeOfCoverage";
  /** The actual type of coverage. */
  private String typeOfCoverage;
  /** The property key for the bonus score weight. */
  public static final String firstTimeKey = "FirstTimeOverThreshold";
  /** The actual bonus score weight. */
  private int firstTimeWeight;
  /** The name of the bonus for going over threshold for the first time. */
  public static final String firstTimeBonusName = "FirstTimeBonus";
  

  /**
   * Creates a new event for Keeping The Coverage High.
   * Ensures all required properties are available. 
   * @throws EventException If the configuration cannot be created. 
   */
  public KeepTheCoverageHigh() throws EventException {
    super(eventName);
    try {
      upperThreshold = Integer.parseInt(configManager.getProperty(eventName, upperThresholdKey));
      typeOfCoverage = configManager.getProperty(eventName, typeOfCoverageKey);
      firstTimeWeight = Integer.parseInt(configManager.getProperty(eventName, firstTimeKey));
    }
    catch (Exception e) {
      throw new EventException("Error configuring properties", e);
    }
  }

  /**
   * Returns the total number of points for this event. 
   * The total is the base score times the base score weight plus
   * the bonus score times the bonus score weight. 
   * @param score The event score. 
   * @return The total points for this event given the current weights. 
   */
  public int computePoints(EventScore score) {
    return ((score.getBaseScore() * this.baseScoreWeight) +
        (score.getBonusScore(firstTimeBonusName) * this.firstTimeWeight));
  }

  /**
   * Creates an EventScore instance for KeepTheCoverageHigh.
   * @param match The match of interest. 
   * @param team The team of interest. 
   * @param timestamp the time at which this event is being run.
   * @return The EventScore.  
   * @throws EventException If problems occur while processing this event. 
   */
  public EventScore computeScore(Match match, Team team, XMLGregorianCalendar timestamp) 
  throws EventException {
    // [0] create the default score, which awards no points. 
    EventScore score = new EventScore(timestamp, match, team);
    // [1] find the project and the sensorbase client.  
    Project project = match.getProject(team);
    SensorBaseClient client = match.getSensorBaseClient(team);
    // [2] get the Build sensor data for the previous interval of time.
    String projectName = project.getName();
    String owner = project.getOwner();
    String sdt = "Build";
    XMLGregorianCalendar endTime = timestamp;
    XMLGregorianCalendar startTime = Tstamp.incrementMinutes(endTime, wakeupInterval * -1);
    SensorDataIndex index;
    this.logger.info(String.format("Getting build sensor data for match %s and team %s",
        match.getName(), team.getName()));
    try {
      index = client.getProjectSensorData(owner, projectName, startTime, endTime, sdt);
    }
    catch (Exception e) {
      throw new EventException("Error retrieving build data.", e);
    }
    // [3] see if there's been a build.
    XMLGregorianCalendar ciBuildTime = getCiBuildTime(client, index);
    
    // [4] if there has not been a build, return the default score right now. 
    if (ciBuildTime == null) {
      score.setExplanation("No recent build, so no score.");
      return score;
    }
    
    // [5] there has been a build, so see if there is coverage since then.
    DailyProjectDataClient dpdClient = match.getDailyProjectDataClient(team);
    this.logger.info(String.format("Getting DPD coverage data for match %s and team %s",
        match.getName(), team.getName()));
    CoverageDailyProjectData coverageDpd;
    try {
      coverageDpd = dpdClient.getCoverage(owner, projectName, ciBuildTime, typeOfCoverage);
    }
    catch (Exception e) {
      throw new EventException(String.format("Error getting coverage for match %s and team %s",
          match.getName(), team.getName()), e);
    }

    // [6] if no coverage since then, return the default score.
    if (!coverageDpd.hasCoverageData()) {
      score.setExplanation("Recent build, but no coverage since then, so no score.");
      return score;
    }
      
    // [7] there is coverage data since the last CI build, so compute the score.
    int currCoverage = coverageDpd.getPercentageCoverage();
    this.logger.info(typeOfCoverage + " coverage is: " + currCoverage);
    score.setBaseScore(computeBaseScore(score, currCoverage));
    score.addBonusScore(firstTimeBonusName, computeBonusScore(score, currCoverage));
    
    // [8] now that we've computed the score, store coverage for the next time we run this event.
    this.lastCoverage = currCoverage;
    this.hasLastCoverage = true;
    // Finally return the final score. 
    return score; 
  }
  
  /**
   * Computes the base score given the current coverage. 
   * If there is no prior coverage, return 0.
   * If the current coverage is over the UpperThreshold, return 0.
   * Otherwise return between -10 and +10 depending upon change in coverage.
   * @param score The EventScore. 
   * @param currCoverage The current coverage percentage.
   * @return The base score. 
   */
  private int computeBaseScore(EventScore score, int currCoverage) {
    if (!this.hasLastCoverage) {
      score.setExplanation("No prior coverage. Base score is 0.");
      return 0;
    }
    if (currCoverage > upperThreshold) {
      score.setExplanation("Current coverage is above threshold. Base score is 0.");
      return 0;
    }
    
    int diff = currCoverage - this.lastCoverage;
    if (diff < -10) {
      score.setExplanation("Coverage has dropped more than 10%. Base score is -10.");
      return -10;
    }
    else if (diff > 10) {
      score.setExplanation("Coverage has increased more than 10%. Base score is +10.");
      return 10;
    }
    else {
      score.setExplanation("Base score reflects coverage change of: " + diff + "%.");
      return diff;
    }
  }
  
  /**
   * Computes the bonus score given the current coverage. 
   * Return 0 if the bonus score has already been given out or if the current coverage is 
   * below the upper threshold.
   * Otherwise, return 1 and set the bonus as having already been provided.
   * @param score The EventScore. 
   * @param currCoverage The current coverage.
   * @return The bonus score. 
   */
  private int computeBonusScore(EventScore score, int currCoverage) {
    if (this.hasFirstTimeBonus) {
      score.appendExplanation("Bonus already awarded; no bonus score.");
      return 0;
    }
  
    if (currCoverage < upperThreshold) {
      score.appendExplanation("Current coverage below threshold; no bonus score.");
      return 0;
    }
    else {
      this.hasFirstTimeBonus = true;
      score.appendExplanation("Coverage above threshold for first time! Bonus score awarded!");
      return 1; 
    }
  }
  
  /**
   * Returns the timestamp of the first CI Build sensor data instance, or null if none was found. 
   * A CI Build sensor data instance has a "Type" property whose value is "continuous.integration".
   * @param client The sensorbase client. 
   * @param index The SensorDataIndex. 
   * @return A timestamp of the first CI Build instance, or null if none was found. 
   */
  private XMLGregorianCalendar getCiBuildTime(SensorBaseClient client, SensorDataIndex index) {
    try {
      for (SensorDataRef ref : index.getSensorDataRef()) {
        SensorData data = client.getSensorData(ref);
        Property buildTypeProperty = data.findProperty("Type");
        if ((buildTypeProperty != null) && 
            (buildTypeProperty.getValue().equals("continuous.integration"))) { 
          return data.getTimestamp();
        }
      }
      return null;
    }
    catch (Exception e) {
      return null;
    }
  }
}
